use e_utils::chrono::{
    china_now, now, parse_datetime_offset, parse_timestamp, DateTime, FixedOffset, TimeZone, Utc,
};

use crate::Packet;
use std::fmt::Display;
use std::io::Cursor;
use std::net::{Ipv4Addr, SocketAddr, SocketAddrV4, ToSocketAddrs, UdpSocket};
use std::time::Duration;

#[derive(Clone, Debug)]
pub struct Client {
    pub packet: Packet,
    pub timestamp: i64,
    pub src: String,
    pub read_timeout: Duration,
    pub write_timeout: Duration,
    pub target: SocketAddr,
    pub offset: Option<FixedOffset>,
    pub format: Option<String>,
}
impl Default for Client {
    fn default() -> Self {
        Self {
            target: SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::LOCALHOST, 123)),
            packet: Packet::new_client(),
            timestamp: 0,
            src: String::from("0.0.0.0:0"),
            write_timeout: Duration::from_secs(5),
            read_timeout: Duration::from_secs(5),
            offset: None,
            format: None,
        }
    }
}
impl Client {
    ///### Example: Get time from an NTP server and sync systemtime
    /// ```rust
    /// fn main() -> e_utils::AnyResult<()> {
    ///     let target = "0.pool.ntp.org:123";
    ///     let res = ntp_client::Client::new()
    ///         .target(target)?
    ///         .format(Some("%Y/%m/%d %H:%M:%S"))
    ///         .request()?;
    ///     let res_str = res.get_datetime_str().ok_or("error")?;
    ///     println!("UTC str -> {res_str}");
    ///     let datetime = res.get_datetime_utc().ok_or("get datetime utc")?;
    ///     ntp_client::sync_systemtime(datetime)?;
    ///     Ok(())
    /// }
    /// ```
    pub fn new() -> Self {
        Self::default()
    }
    pub fn offset(mut self, offset: Option<FixedOffset>) -> Self {
        self.offset = offset;
        self
    }
    pub fn format(mut self, format: Option<impl Into<String>>) -> Self {
        self.format = format.map(|v| v.into());
        self
    }
    pub fn target<A: ToSocketAddrs>(mut self, addr: A) -> e_utils::AnyResult<Self> {
        self.target = addr
            .to_socket_addrs()?
            .next()
            .ok_or("to_socket_addrs next")?;
        Ok(self)
    }
    pub fn src_ip(mut self, addr: impl Into<String>) -> Self {
        self.src = addr.into();
        self
    }
    pub fn packet(mut self, packet: Packet) -> Self {
        self.packet = packet;
        self
    }
    
    pub fn request(mut self) -> e_utils::AnyResult<Self> {
        let data: Vec<u8> = self.packet.clone().into();
        let sock = UdpSocket::bind(&self.src)?;
        sock.set_read_timeout(Some(self.read_timeout))?;
        sock.set_write_timeout(Some(self.write_timeout))?;
        let _sz = sock.send_to(&data, self.target)?;
        let mut buf = vec![0; 48];
        let _res = sock.recv(&mut buf)?;
        let rdr = Cursor::new(&buf);
        self.packet = Packet::try_from(rdr)?;
        self.timestamp = DateTime::<Utc>::from(self.packet.transmit_time).timestamp();
        Ok(self)
    }
    // UTC 时间获取保持原样
    pub fn get_datetime_utc(&self) -> Option<DateTime<Utc>> {
        DateTime::from_timestamp(self.timestamp, 0)
    }
    // UTC 时间获取保持原样
    pub fn get_datetime_shanghai(&self) -> Option<DateTime<FixedOffset>> {
        parse_timestamp(self.get_datetime_utc()?.timestamp())
    }
    fn format_datetime<T: TimeZone>(dt: &DateTime<T>, format: &Option<String>) -> String
    where
        T::Offset: Display,
    {
        match format {
            Some(fmt) => dt.format(fmt).to_string(),
            None => dt.to_string(),
        }
    }
    // 统一格式化入口
    pub fn get_datetime_str(&self) -> Option<String> {
        Some(match self.offset {
            Some(offset) => Self::format_datetime(
                &parse_datetime_offset(self.get_datetime_utc()?.naive_utc(), offset)?,
                &self.format,
            ),
            None => Self::format_datetime(&self.get_datetime_utc()?, &self.format),
        })
    }
    pub fn now_zh() -> Option<DateTime<FixedOffset>> {
        china_now()
    }
    pub fn now() -> DateTime<Utc> {
        now()
    }
}

#[test]
fn test_request_ntp_org() {
    let res = Client::new()
        .target("0.pool.ntp.org:123")
        .unwrap()
        .request()
        .unwrap();
    res.get_datetime_str().unwrap();
}
